﻿using System;
using System.Threading;
using System.Threading.Tasks;
using Elderos.Utils.Logging;
using Timer = System.Timers.Timer;

namespace Elderos.Utils.Services
{
	/// <summary>
	/// A windows service, which provides basic algorithm for performing tasks getting out from a queue
	/// </summary>
	/// <typeparam name="TQueueElement"></typeparam>
	/// <typeparam name="TResult"></typeparam>
	public abstract class TaskQueueService<TQueueElement, TResult> : WindowsServiceBase
		where TQueueElement : class
		where TResult : class
	{
		protected ManualResetEventSlim CanContinue = new ManualResetEventSlim(true);

		public event Action OnStopEvent;

		public void InvokeOnStopEvent()
		{
			Action handler = OnStopEvent;
			if (handler != null) handler.Invoke();
		}

		protected override void OnStart(string[] args)
		{
			base.OnStart(args);
			_queueTask.Start();

            _tasksDoneTimer = new Timer(60000D);
            _tasksDoneTimer.Elapsed +=
                (x, y) =>
                {
                    int count = 0;
                    count = Interlocked.Exchange(ref _tasksDone, count);
                    Logger.Info("Total {0} tasks done for the last minute.", count);
                };

			_tasksDoneTimer.Start();
			_queueTask.Wait();
		}

		/// <summary>
		/// Method cancels all the tasks being processes and calls base OnStop method.
		/// It is sealed by this reason, if you want to perform some actions before, use OnStopEvent.
		/// </summary>
		protected override sealed void OnStop()
		{
			Logger.Info("Stopping service...");
			InvokeOnStopEvent();
			try
			{
				_cancellationTokenSource.Cancel();
				_queueTask.Wait();
			}
			catch (AggregateException ex)
			{
				foreach (Exception exception in ex.Flatten().InnerExceptions)
				{
					if (exception is OperationCanceledException)
						Logger.Info(ex);
					else Logger.Error(ex);
				}
			}

            if (_tasksDoneTimer != null) 
                _tasksDoneTimer.Dispose();

		    base.OnStop();
			Logger.Info("Service stopped");
		}

		private void Process(CancellationTokenSource token)
		{
			Logger.Info("Starting QueueManager.");
			Thread.CurrentThread.Name = "QueueManager";
			while (true)
			{
				TQueueElement queueElement = null;
				try
				{
					if (_working >= _maxTasksCount)
						CanContinue.Wait();
					queueElement = Dequeue();
					token.Token.ThrowIfCancellationRequested();
					if (queueElement == null)
					{
						Logger.Info("The queue is empty.");
						Thread.Sleep(_sleepingTimeout);
						continue;
					}
					while (true)
					{
						token.Token.ThrowIfCancellationRequested();
						Interlocked.Increment(ref _working);
						if (_working >= _maxTasksCount)
							CanContinue.Reset();
						CanContinue.Wait();
						var taskThread = new Task(() => ProcessTaskWrap(queueElement, token), token.Token,
												  TaskCreationOptions.AttachedToParent);
						token.Token.ThrowIfCancellationRequested();
						taskThread.Start();
						break;
					}
				}
				catch (OperationCanceledException)
				{
					if (queueElement != null)
						Cancel(queueElement);
					break;
				}
				catch (Exception ex)
				{
					Logger.Error(ex);
				}
			}
		}

		/// <summary>
		/// Processes the <paramref name="queueElement">task</paramref>.
		/// </summary>
		/// <param name="queueElement">Task to be processed</param>
		/// <param name="token">Cancellation token for cancellation support</param>
		/// <returns>Result of processing</returns>
		private void ProcessTaskWrap(TQueueElement queueElement, CancellationTokenSource token)
		{
			try
			{
				token.Token.ThrowIfCancellationRequested();
				TResult result = ProcessTask(queueElement, token);
				token.Token.ThrowIfCancellationRequested();
				Save(queueElement, result);
			}
			catch (Exception ex)
			{
				if (ex is OperationCanceledException)
					throw;
				Logger.Error("Task: " + queueElement, ex);
				Fail(queueElement);
			}
			finally
			{
				Interlocked.Decrement(ref _working);
				Interlocked.Increment(ref _tasksDone);
				CanContinue.Set();
			}
		}

		protected override void Dispose(bool disposing)
		{
            if (_tasksDoneTimer != null)
			    _tasksDoneTimer.Dispose();
			base.Dispose(disposing);
		}

		#region Abstract members

		/// <summary>
		/// Dequeues a queue element to be processed as a task
		/// </summary>
		/// <returns>A queue task, which can be processed</returns>
        protected abstract TQueueElement Dequeue();

		protected abstract TResult ProcessTask(TQueueElement queueElement, CancellationTokenSource token);

		/// <summary>
		/// Saves the processed task
		/// </summary>
		/// <param name="queueElement">Processed queue task</param>
		/// <param name="result">Result returned by the ProcessTaskWrap method</param>
		protected abstract void Save(TQueueElement queueElement, TResult result);

		/// <summary>
		/// Marks the task as failed in the queue
		/// </summary>
		/// <param name="queueElement">Element to be marked as failed</param>
        protected abstract void Fail(TQueueElement queueElement);

		/// <summary>
		/// Marks the task as cancelled (usually it must be returned into the initial state)
		/// </summary>
		/// <param name="queueElement">Element to be marked as cancelled</param>
        protected abstract void Cancel(TQueueElement queueElement);

		#endregion

		#region Properties & fields

		private readonly CancellationTokenSource _cancellationTokenSource;

		private readonly int _maxTasksCount;
		private readonly Task _queueTask;
		private readonly TimeSpan _sleepingTimeout;

		private Timer _tasksDoneTimer;
		private int _tasksDone;
		private int _working;

		#endregion

		#region Constructors

		/// <summary>
		/// Creates a new TaskQueueService with specified task waiting timeout,
		/// max tasks count and dequeueing argument object. 
		/// </summary>
		/// <param name="sleepingTimeout">The timeout to wait when the task can't be processed. 10s by default.</param>
		/// <param name="maxTasksCount">Maximum count of task to be processed at once. By default it equals the processors number.</param>
		protected TaskQueueService(TimeSpan? sleepingTimeout = null, int maxTasksCount = -1)
		{
			lock (this)
			{
				Logger.Info("Initializing TaskQueue service...");
				_cancellationTokenSource = new CancellationTokenSource();
				if (maxTasksCount == -1)
					_maxTasksCount = Environment.ProcessorCount + 1;
				else _maxTasksCount = maxTasksCount;

				if (sleepingTimeout == null)
					sleepingTimeout = TimeSpan.FromSeconds(10);
				_sleepingTimeout = sleepingTimeout.Value;
				_queueTask = new Task(() => Process(_cancellationTokenSource), TaskCreationOptions.LongRunning);
				Logger.Info("TaskQueueService initialized.");
			}
		}

		#endregion
	}
}